// Protocol Buffers for GWT
// Copyright 2010 Thomas Broyer  All rights reserved.
// http://code.google.com/p/protobuf-gwt/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package net.ltgt.gwt.protobuf.compiler.client;

import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.EnumDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
import com.google.protobuf.compiler.JavaHelpers;

import net.ltgt.gwt.protobuf.compiler.CustomFieldSerializerGenerator;

import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @author t.broyer@ltgt.net Thomas Broyer
 * Based on the initial work of:
 * @author kenton@google.com Kenton Varda
 * Based on original Protocol Buffers design by
 * Sanjay Ghemawat, Jeff Dean, and others.
 */
public class MessageGenerator extends Generator {

  private final Descriptor descriptor;
  private final SortedMap<FieldDescriptor, FieldGenerator> fieldGenerators;

  public MessageGenerator(Descriptor descriptor) {
    super(descriptor.getName());
    this.descriptor = descriptor;
    this.fieldGenerators = createFieldGeneratorMap();
    // TODO: extensions?
  }

  private SortedMap<FieldDescriptor, FieldGenerator> createFieldGeneratorMap() {
    SortedMap<FieldDescriptor, FieldGenerator> ret = new TreeMap<FieldDescriptor, FieldGenerator>();
    for (FieldDescriptor field : descriptor.getFields()) {
      ret.put(field, makeGenerator(field));
    }
    return ret;
  }

  private FieldGenerator makeGenerator(FieldDescriptor field) {
    if (field.isRepeated()) {
      switch (field.getJavaType()) {
        case MESSAGE:
          return new RepeatedMessageFieldGenerator(field);
        case ENUM:
          return new RepeatedEnumFieldGenerator(field);
        default:
          return new RepeatedPrimitiveFieldGenerator(field);
      }
    } else {
      switch (field.getJavaType()) {
        case MESSAGE:
          return new MessageFieldGenerator(field);
        case ENUM:
          return new EnumFieldGenerator(field);
        default:
          return new PrimitiveFieldGenerator(field);
      }
    }
  }

  @Override
  public void generate(Writer output) throws IOException {
    boolean isOwnFile = descriptor.getContainingType() == null
        && descriptor.getFile().getOptions().getJavaMultipleFiles();

    output.write("public ");
    if (!isOwnFile) {
      output.write("static ");
    }
    output.write("class " + descriptor.getName()
        + " extends com.google.protobuf.GeneratedMessage");
    if (!JavaHelpers.hasDescriptorMethods(descriptor)) {
      output.write("Lite");
    }
    output.write(" {\n");

    output.write("// Use " + descriptor.getName()
        + ".newBuilder() to construct.\n");
    output.write("private " + descriptor.getName() + "() {}\n");

    output.write("\n");
    output.write("private static " + descriptor.getName()
        + " defaultInstance;\n");
    output.write("public static " + descriptor.getName()
        + " getDefaultInstance() {\n");
    output.write("  if (defaultInstance == null) {\n");
    output.write("    defaultInstance = new " + descriptor.getName() + "();\n");
    output.write("  }\n");
    output.write("  return defaultInstance;\n");
    output.write("}\n");
    output.write("public " + descriptor.getName()
        + " getDefaultInstanceForType() {\n");
    output.write("  return getDefaultInstance();\n");
    output.write("}\n");
    // called from builder
    output.write("public static boolean isDefaultInstance("
        + descriptor.getName() + " other) {\n");
    output.write("  return other == defaultInstance && defaultInstance != null;\n");
    output.write("}\n");
    output.write("\n");

    // descriptors
    if (JavaHelpers.hasDescriptorMethods(descriptor)) {
      output.write("public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() {\n");
      output.write("  return getDescriptor();\n");
      output.write("}\n");
      output.write("public static final com.google.protobuf.Descriptors.Descriptor\n");
      output.write("   getDescriptor() {\n");
      output.write("  return Descriptor.INSTANCE;\n");
      output.write("}\n");
      output.write("\n");
      output.write("private static final class Descriptor extends com.google.protobuf.Descriptors.Descriptor {\n");
      output.write("  public static final Descriptor INSTANCE = new Descriptor();\n");
      output.write("\n");
      output.write("  public int getIndex() { return " + descriptor.getIndex() + "; }\n");
      output.write("  public String getName() { return \"" + descriptor.getName() + "\"; }\n");
      if (descriptor.getContainingType() != null) {
        output.write("  public com.google.protobuf.Descriptors.Descriptor getContainingType() {\n");
        output.write("    return " + JavaHelpers.getClassName(descriptor.getContainingType()) + ".getDescriptor();\n");
        output.write("  }\n");
      } else {
        output.write("  public String getFullName() { return \"" + descriptor.getFullName() + "\"; }\n");
        output.write("  public com.google.protobuf.Descriptors.FileDescriptor getFile() {\n");
        output.write("    return " + JavaHelpers.getClassName(descriptor.getFile()) + ".getDescriptor();\n");
        output.write("  }\n");
      }
      output.write("  protected com.google.protobuf.Descriptors.FieldDescriptor[] createFields() {\n");
      output.write("    return new com.google.protobuf.Descriptors.FieldDescriptor[] {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateDescriptorCode(output);
      }
      output.write("    };\n");
      output.write("  }\n");
      output.write("  protected com.google.protobuf.Descriptors.Descriptor[] createNestedTypes() {\n");
      output.write("    return new com.google.protobuf.Descriptors.Descriptor[] {\n");
      for (Descriptor nestedType : descriptor.getNestedTypes()) {
        output.write("      " + JavaHelpers.getClassName(nestedType) + ".getDescriptor(),");
      }
      output.write("    };\n");
      output.write("  }\n");
      output.write("  protected com.google.protobuf.Descriptors.EnumDescriptor[] createEnumTypes() {\n");
      output.write("    return new com.google.protobuf.Descriptors.EnumDescriptor[] {\n");
      for (EnumDescriptor enumType : descriptor.getEnumTypes()) {
        output.write("      " + JavaHelpers.getClassName(enumType) + ".getDescriptor(),");
      }
      output.write("    };\n");
      output.write("  }\n");
      output.write("}\n");

      output.write("protected Object internalGetField(int tag) {\n");
      output.write("  switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateGetFieldCode(output);
      }
      output.write("    default: throw new UnsupportedOperationException();\n");
      output.write("  }\n");
      output.write("}\n");
      output.write("protected Object internalGetRepeatedField(int tag, int index) {\n");
      output.write("  switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateGetRepeatedFieldCode(output);
      }
      output.write("    default: throw new UnsupportedOperationException();\n");
      output.write("  }\n");
      output.write("}\n");
      output.write("protected int internalGetRepeatedFieldCount(int tag) {\n");
      output.write("  switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateGetRepeatedCountFieldCode(output);
      }
      output.write("    default: throw new UnsupportedOperationException();\n");
      output.write("  }\n");
      output.write("}\n");
      output.write("protected boolean internalHasField(int tag) {\n");
      output.write("  switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateHasFieldCode(output);
      }
      output.write("    default: throw new UnsupportedOperationException();\n");
      output.write("  }\n");
      output.write("}\n");
    }

    // nested types
    for (EnumDescriptor enumType : descriptor.getEnumTypes()) {
      new EnumGenerator(enumType).generate(output);
    }
    for (Descriptor nestedType : descriptor.getNestedTypes()) {
      new MessageGenerator(nestedType).generate(output);
      new CustomFieldSerializerGenerator(nestedType).generate(output);
    }
    // TODO: extensions

    // Fields
    for (FieldDescriptor field : descriptor.getFields()) {
      output.write("public static final int "
          + JavaHelpers.getFieldConstantName(field) + " = " + field.getNumber()
          + ";\n");
      fieldGenerators.get(field).generateMembers(output);
    }

    // TODO: init fields?

    if (JavaHelpers.hasGeneratedMethods(descriptor)) {
      generateIsInitialized(output);
      generateMessageSerializationMethods(output);
    }

    generateParseFromMethods(output);
    generateBuilder(output);

    output.write("\n");
    output.write("// @@protoc_insertion_point(class_scope:"
        + descriptor.getFullName() + ")\n");
    // outdent
    output.write("}\n\n");
  }

  protected void generateBuilder(Writer output) throws IOException {
    output.write("public static Builder newBuilder() { return Builder.create(); }\n");
    output.write("public Builder newBuilderForType() { return newBuilder(); }\n");
    output.write("public static Builder newBuilder(" + descriptor.getName()
        + " prototype) {\n");
    output.write("  return newBuilder().mergeFrom(prototype);\n");
    output.write("}\n");
    output.write("public Builder toBuilder() { return newBuilder(this); }\n");
    output.write("\n");

    // TODO: extensions

    output.write("public static final class Builder extends com.google.protobuf.GeneratedMessage");
    if (!JavaHelpers.hasDescriptorMethods(descriptor)) {
      output.write("Lite");
    }
    output.write(".Builder<Builder> {\n");
    // indent
    generateCommonBuilderMethods(output);

    if (JavaHelpers.hasGeneratedMethods(descriptor)) {
      generateBuilderParsingMethods(output);
    }

    for (FieldDescriptor field : descriptor.getFields()) {
      output.write("\n");
      fieldGenerators.get(field).generateBuilderMembers(output);
    }

    output.write("\n");
    output.write("// @@protoc_insertion_point(builder_scope:"
        + descriptor.getFullName() + ")\n");
    // outdent
    output.write("}\n");
  }

  private void generateCommonBuilderMethods(Writer output) throws IOException {
    output.write("private " + descriptor.getName() + " result;\n");
    output.write("\n");
    output.write("private Builder() {}\n");
    output.write("\n");
    output.write("private static Builder create() {\n");
    output.write("  Builder builder = new Builder();");
    output.write("  builder.result = new " + descriptor.getName() + "();\n");
    output.write("  return builder;\n");
    output.write("}\n");
    output.write("\n");
    output.write("public Builder clear() {\n");
    output.write("  if (result == null) { throw new IllegalStateException(); }\n");
    output.write("  result = new " + descriptor.getName() + "();\n");
    output.write("  return this;\n");
    output.write("}\n");
    output.write("\n");
    output.write("public Builder clone() {\n");
    output.write("  return create().mergeFrom(result);\n");
    output.write("}\n");
    output.write("\n");

    // TODO: descriptor?

    output.write("public " + descriptor.getName()
        + " getDefaultInstanceForType() {\n");
    output.write(" return " + descriptor.getName() + ".getDefaultInstance();\n");
    output.write("}\n");
    output.write("\n");
    output.write("public boolean isInitialized() { return result.isInitialized(); }\n");
    output.write("\n");

    output.write("public " + descriptor.getName() + " build() {\n");
    output.write("  if (result != null && !result.isInitialized()) {\n");
    output.write("    throw new com.google.protobuf.UninitializedMessageException();\n");
    output.write("  }\n");
    output.write("  return buildPartial();\n");
    output.write("}\n");
    output.write("\n");
    output.write("public " + descriptor.getName() + " buildParsed()\n");
    output.write("    throws com.google.protobuf.InvalidProtocolBufferException {\n");
    output.write("  if (!isInitialized()) {\n");
    output.write("    throw new com.google.protobuf.InvalidProtocolBufferException();\n");
    output.write("  }\n");
    output.write("  return buildPartial();");
    output.write("}\n");
    output.write("\n");
    output.write("public " + descriptor.getName() + " buildPartial() {\n");
    output.write("  if (result == null) {\n");
    output.write("    throw new IllegalStateException();\n");
    output.write("  }\n");
    for (FieldDescriptor field : descriptor.getFields()) {
      fieldGenerators.get(field).generateBuildingCode(output);
    }
    output.write("  " + descriptor.getName() + " returnMe = result;\n");
    output.write("  result = null;\n");
    output.write("  return returnMe;");
    output.write("}\n");
    output.write("\n");
    
    if (JavaHelpers.hasDescriptorMethods(descriptor)) {
      output.write("  protected com.google.protobuf.GeneratedMessage internalGetResult() { return result; }\n");
      output.write("  public void internalAddRepeatedField(int tag, Object value) {\n");
      output.write("    switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateAddRepeatedFieldCode(output);
      }
      output.write("    }\n");
      output.write("  }\n");
      output.write("  public void internalClearField(int tag) {\n");
      output.write("    switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateClearFieldCode(output);
      }
      output.write("    }\n");
      output.write("  }\n");
      output.write("  public void internalSetField(int tag, Object value) {\n");
      output.write("    switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateSetFieldCode(output);
      }
      output.write("    }\n");
      output.write("  }\n");
      output.write("\n");
      output.write("  public void internalSetRepeatedField(int tag, int index, Object value) {\n");
      output.write("    switch (tag) {\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateSetRepeatedFieldCode(output);
      }
      output.write("    }\n");
      output.write("  }\n");
    }
    
    if (JavaHelpers.hasGeneratedMethods(descriptor)) {
      if (JavaHelpers.hasDescriptorMethods(descriptor)) {
        // TODO: mergeFrom(com.google.protobuf.Message other)
      }

      output.write("public Builder mergeFrom(" + descriptor.getName()
          + " other) {\n");
      output.write("  if (" + descriptor.getName()
          + ".isDefaultInstance(other)) return this;\n");
      for (FieldDescriptor field : descriptor.getFields()) {
        fieldGenerators.get(field).generateMergingCode(output);
      }
      // TODO: extensions
      // TODO: unknown fields
      output.write("  return this;\n");
      output.write("}\n");
    }
  }

  public void generateBuilderParsingMethods(Writer output) throws IOException {
    output.write("public Builder mergeFrom(\n");
    output.write("    com.google.gwt.user.client.rpc.SerializationStreamReader streamReader)\n");
    output.write("    throws com.google.gwt.user.client.rpc.SerializationException {\n");
    output.write("  while (true) {\n");
    output.write("    int tag = streamReader.readInt();\n");
    output.write("    switch (tag) {\n");
    output.write("      case 0:\n");
    output.write("        return this;\n");
    output.write("      default:\n");
    // TODO: unknown fields
    output.write("        throw new com.google.gwt.user.client.rpc.SerializationException();\n");

    for (FieldDescriptor field : descriptor.getFields()) {
      fieldGenerators.get(field).generateParsingCode(output);
    }

    output.write("    }\n");
    output.write("  }\n");
    output.write("}\n");
  }

  private void generateIsInitialized(Writer output) throws IOException {
    output.write("public boolean isInitialized() {\n");

    for (FieldDescriptor field : descriptor.getFields()) {
      if (field.isRequired()) {
        output.write("if (!has"
            + JavaHelpers.underscoresToCapitalizedCamelCase(field)
            + "()) return false;\n");
      }
    }

    for (FieldDescriptor field : descriptor.getFields()) {
      if (field.getJavaType() == JavaType.MESSAGE
          && hasRequiredFields(field.getMessageType())) {
        if (field.isRequired()) {
          // hasXxx already checked above
          output.write("if (!get"
              + JavaHelpers.underscoresToCapitalizedCamelCase(field)
              + "().isInitialized()) return false;\n");
        } else if (field.isOptional()) {
          String capitalizedCamelCase = JavaHelpers.underscoresToCapitalizedCamelCase(field);
          output.write("if (has" + capitalizedCamelCase + "() && !get"
              + capitalizedCamelCase + "().isInitialized()) return false;\n");
        } else if (field.isRepeated()) {
          output.write("for ("
              + JavaHelpers.getClassName(field.getMessageType())
              + " element : get"
              + JavaHelpers.underscoresToCapitalizedCamelCase(field)
              + "List()) {\n");
          output.write("  if (!element.isInitialized()) return false;");
          output.write("}\n");
        }
      }
    }

    // TODO: extensions

    output.write("  return true;");
    output.write("}\n");
  }

  private static boolean hasRequiredFields(Descriptor descriptor,
      Set<Descriptor> alreadySeen) {
    if (!alreadySeen.add(descriptor)) {
      return false;
    }
    for (FieldDescriptor field : descriptor.getFields()) {
      if (field.isRequired()) {
        return true;
      }
      if (field.getJavaType() == JavaType.MESSAGE) {
        if (hasRequiredFields(field.getMessageType(), alreadySeen)) {
          return true;
        }
      }
    }
    return false;
  }

  private static boolean hasRequiredFields(Descriptor descriptor) {
    HashSet<Descriptor> alreadSeen = new HashSet<Descriptor>();
    return hasRequiredFields(descriptor, alreadSeen);
  }

  public void generateParseFromMethods(Writer output) throws IOException {
    output.write("public static " + descriptor.getName() + " parseFrom(com.google.gwt.user.client.rpc.SerializationStreamReader streamReader)\n");
    output.write("    throws com.google.gwt.user.client.rpc.SerializationException {\n");
    // TODO: optimize by not instantiating a builder
    output.write("  return newBuilder().mergeFrom(streamReader).build();\n");
    output.write("}\n");
  }

  public void generateMessageSerializationMethods(Writer output)
      throws IOException {
    output.write("public void writeTo(com.google.gwt.user.client.rpc.SerializationStreamWriter streamWriter)\n");
    output.write("                    throws com.google.gwt.user.client.rpc.SerializationException {\n");
    // indent

    // TODO: extensions

    for (FieldDescriptor field : fieldGenerators.keySet()) {
      generateSerializeOneField(field, output);
    }

    // TODO: unknown fields
    
    // end-of-message marker
    output.write("  streamWriter.writeInt(0);\n");

    // outdent
    output.write("}\n");
  }

  private void generateSerializeOneField(FieldDescriptor field, Writer output)
      throws IOException {
    fieldGenerators.get(field).generateSerializationCode(output);
  }
}
